/**
 * 
 */
package qy.jalgotrade.stratanalyzer;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.google.common.math.DoubleMath;

import qy.jalgotrade.broker.Broker;
import qy.jalgotrade.broker.Order;
import qy.jalgotrade.broker.OrderEvent;
import qy.jalgotrade.broker.OrderExecutionInfo;
import qy.jalgotrade.event.EventHandler;
import qy.jalgotrade.stratanalyzer.returns.PositionTracker;
import qy.jalgotrade.strategy.BaseStrategy;
import qy.jalgotrade.utils.CommonUtils;

/**
 * <pre>
 * A :class:`pyalgotrade.stratanalyzer.StrategyAnalyzer` that records the profit/loss and returns of
 * every completed trade.
 * 
 * .. note::
 *     This analyzer operates on individual completed trades. For example, lets say you start
 * with a $1000 cash, and then you buy 1 share of XYZ for $10 and later sell it for $20:
 * 
 *     * The trade's profit was $10.
 *     * The trade's return is 100%, even though your whole portfolio went from $1000 to $1020, a 2% return.
 * </pre>
 * 
 * @author c-geo
 *
 */
public class Trades extends StrategyAnalyzer implements EventHandler {

	// TODO: to optimize
	private List<Double> __all;

	private List<Double> __profits;

	private List<Double> __losses;

	private List<Double> __allReturns;

	private List<Double> __positiveReturns;

	private List<Double> __negativeReturns;

	private List<Double> __allCommissions;

	private List<Double> __profitableCommissions;

	private List<Double> __unprofitableCommissions;

	private List<Double> __evenCommissions;

	private int __evenTrades;

	private Map<String, PositionTracker> __posTrackers;

	/**
	 * 
	 */
	public Trades() {

		super();
		__all = new ArrayList<>();
		__profits = new ArrayList<>();
		__losses = new ArrayList<>();
		__allReturns = new ArrayList<>();
		__positiveReturns = new ArrayList<>();
		__negativeReturns = new ArrayList<>();
		__allCommissions = new ArrayList<>();
		__profitableCommissions = new ArrayList<>();
		__unprofitableCommissions = new ArrayList<>();
		__evenCommissions = new ArrayList<>();
		__evenTrades = 0;
		__posTrackers = new HashMap<>();
	}

	/* (non-Javadoc)
	 * @see qy.jalgotrade.event.EventHandler#handle(java.lang.String, java.util.Map)
	 */
	@Override
	public void handle(String eventName, Map<String, Object> params) throws Exception {

		Broker broker = (Broker) params.get("broker");
		OrderEvent orderEvent = (OrderEvent) params.get("orderEvent");
		__onOrderEvent(broker, orderEvent);
	}

	private void __updateTrades(PositionTracker posTracker) {

		double price = 0; // The price doesn't matter since the position should be closed.
		assert DoubleMath.fuzzyEquals(posTracker.getPosition(), 0, CommonUtils.FUZZY_EQUAL_TOLERANCE_MATH);
		double netProfit = posTracker.getPnL(price);
		double netReturn = posTracker.getReturn(price);

		if (DoubleMath.fuzzyCompare(netProfit, 0.0, CommonUtils.FUZZY_EQUAL_TOLERANCE_MATH) > 0) {
			__profits.add(netProfit);
			__positiveReturns.add(netReturn);
			__profitableCommissions.add(posTracker.getCommissions());
		} else if (DoubleMath.fuzzyCompare(netProfit, 0.0, CommonUtils.FUZZY_EQUAL_TOLERANCE_MATH) < 0) {
			__losses.add(netProfit);
			__negativeReturns.add(netReturn);
			__unprofitableCommissions.add(posTracker.getCommissions());
		} else {
			__evenTrades++;
			__evenCommissions.add(posTracker.getCommissions());
		}

		__all.add(netProfit);
		__allReturns.add(netReturn);
		__allCommissions.add(posTracker.getCommissions());

		posTracker.reset();
	}

	private void __updatePosTracker(PositionTracker posTracker, double price, double commission, double quantity) {

		double currentShares = posTracker.getPosition();

		if (DoubleMath.fuzzyCompare(currentShares, 0, CommonUtils.FUZZY_EQUAL_TOLERANCE_MATH) > 0) { // Current position is long

			if (DoubleMath.fuzzyCompare(quantity, 0, CommonUtils.FUZZY_EQUAL_TOLERANCE_MATH) > 0) { // Increase long position

				posTracker.buy(quantity, price, commission);
			} else {

				double newShares = currentShares + quantity;
				if (DoubleMath.fuzzyEquals(newShares, 0, CommonUtils.FUZZY_EQUAL_TOLERANCE_MATH)) { // Exit long.

					posTracker.sell(currentShares, price, commission);
					__updateTrades(posTracker);
				} else if (DoubleMath.fuzzyCompare(newShares, 0, CommonUtils.FUZZY_EQUAL_TOLERANCE_MATH) > 0) { // Sell some shares.

					posTracker.sell(-quantity, price, commission);
				} else { // Exit long and enter short. Use proportional commissions.

					double proportionalCommission = commission * currentShares / -quantity;
					posTracker.sell(currentShares, price, proportionalCommission);
					__updateTrades(posTracker);

					proportionalCommission = commission * newShares / quantity;
					posTracker.sell(-newShares, price, proportionalCommission);
				}
			}
		} else if (DoubleMath.fuzzyCompare(currentShares, 0, CommonUtils.FUZZY_EQUAL_TOLERANCE_MATH) < 0) { // Current position is short

			if (DoubleMath.fuzzyCompare(quantity, 0, CommonUtils.FUZZY_EQUAL_TOLERANCE_MATH) < 0) { // Increase short position

				posTracker.sell(-quantity, price, commission);
			} else {

				double newShares = currentShares + quantity;
				if (DoubleMath.fuzzyEquals(newShares, 0, CommonUtils.FUZZY_EQUAL_TOLERANCE_MATH)) { // Exit short.

					posTracker.buy(-currentShares, price, commission);
					__updateTrades(posTracker);
				} else if (DoubleMath.fuzzyCompare(newShares, 0, CommonUtils.FUZZY_EQUAL_TOLERANCE_MATH) < 0) { // Re-buy some shares.

					posTracker.buy(quantity, price, commission);
				} else { // Exit short and enter long. Use proportional commissions.

					double proportionalCommission = commission * -currentShares / quantity;
					posTracker.buy(-currentShares, price, proportionalCommission);
					__updateTrades(posTracker);

					proportionalCommission = commission * newShares / quantity;
					posTracker.buy(newShares, price, proportionalCommission);
				}
			}
		} else if (DoubleMath.fuzzyCompare(quantity, 0, CommonUtils.FUZZY_EQUAL_TOLERANCE_MATH) > 0) {

			posTracker.buy(quantity, price, commission);
		} else {

			posTracker.sell(-quantity, price, commission);
		}
	}

	private void __onOrderEvent(Broker broker, OrderEvent orderEvent) {

		// Only interested in filled or partially filled orders.
		if (!Arrays.asList(OrderEvent.Type.PARTIALLY_FILLED, OrderEvent.Type.FILLED)
		        .contains(orderEvent.getEventType())) {
			return;
		}

		Order order = orderEvent.getOrder();

		// Get or create the tracker for this instrument.
		PositionTracker posTracker = __posTrackers.get(order.getInstrument());
		if (posTracker == null) {
			posTracker = new PositionTracker(order.getInstrumentTraits());
			__posTrackers.put(order.getInstrument(), posTracker);
		}

		// Update the tracker for this order.
		OrderExecutionInfo execInfo = orderEvent.getEventInfo();
		double price = execInfo.getPrice();
		double commission = execInfo.getCommission();
		Order.Action action = order.getAction();
		double quantity = 0;
		if (Arrays.asList(Order.Action.BUY, Order.Action.BUY_TO_COVER).contains(action)) {
			quantity = execInfo.getQuantity();
		} else if (Arrays.asList(Order.Action.SELL, Order.Action.SELL_SHORT).contains(action)) {
			quantity = execInfo.getQuantity() * -1;
		} else { // Unknown action
			assert false;
		}

		__updatePosTracker(posTracker, price, commission, quantity);
	}

	/*
	 * (non-Javadoc)
	 * @see qy.jalgotrade.stratanalyzer.StrategyAnalyzer#attached(qy.jalgotrade.strategy.BaseStrategy)
	 */
	@Override
	public void attached(BaseStrategy strat) {

		strat.getBroker().getOrderUpdatedEvent().subscribe(this);
	}

	/**
	 * Returns the total number of trades.
	 * 
	 * @return
	 */
	public int getCount() {

		return __all.size();
	}

	/**
	 * Returns the number of profitable trades.
	 * 
	 * @return
	 */
	public int getProfitableCount() {

		return __profits.size();
	}

	/**
	 * Returns the number of unprofitable trades.
	 * 
	 * @return
	 */
	public int getUnprofitableCount() {

		return __losses.size();
	}

	/**
	 * Returns the number of trades whose net profit was 0.
	 * 
	 * @return
	 */
	public int getEvenCount() {

		return __evenTrades;
	}

	/**
	 * Returns a numpy.array with the profits/losses for each trade.
	 * 
	 * @return
	 */
	public List<Double> getAll() {

		return __all;
	}

	/**
	 * Returns a numpy.array with the profits for each profitable trade.
	 * 
	 * @return
	 */
	public List<Double> getProfits() {

		return __profits;
	}

	/**
	 * Returns a numpy.array with the losses for each unprofitable trade.
	 * 
	 * @return
	 */
	public List<Double> getLosses() {

		return __losses;
	}

	/**
	 * Returns a numpy.array with the returns for each trade.
	 * 
	 * @return
	 */
	public List<Double> getAllReturns() {

		return __allReturns;
	}

	/**
	 * Returns a numpy.array with the positive returns for each trade.
	 * 
	 * @return
	 */
	public List<Double> getPositiveReturns() {

		return __positiveReturns;
	}

	/**
	 * Returns a numpy.array with the negative returns for each trade.
	 * 
	 * @return
	 */
	public List<Double> getNegativeReturns() {

		return __negativeReturns;
	}

	/**
	 * Returns a numpy.array with the commissions for each trade.
	 * 
	 * @return
	 */
	public List<Double> getCommissionsForAllTrades() {

		return __allCommissions;
	}

	/**
	 * Returns a numpy.array with the commissions for each profitable trade.
	 * 
	 * @return
	 */
	public List<Double> getCommissionsForProfitableTrades() {

		return __profitableCommissions;
	}

	/**
	 * Returns a numpy.array with the commissions for each unprofitable trade.
	 * 
	 * @return
	 */
	public List<Double> getCommissionsForUnprofitableTrades() {

		return __unprofitableCommissions;
	}

	/**
	 * Returns a numpy.array with the commissions for each trade whose net profit was 0.
	 * 
	 * @return
	 */
	public List<Double> getCommissionsForEvenTrades() {

		return __evenCommissions;
	}
}
